﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

using FirstFloor.Documents.IO;
using FirstFloor.Documents.Pdf.Fonts;
using FirstFloor.Documents.Pdf.Functions;
using System.Windows.Markup;
using System.Windows;

namespace FirstFloor.Documents.Pdf
{
    /// <summary>
    /// Parses PDF documents.
    /// </summary>
    internal class PdfParser
        : IDisposable
    {
        private static Dictionary<string, object> inheritablePageAttributes = CreateInheritablePageAttributes("Resources", "MediaBox", "CropBox", "Rotate");

        private PdfLexer lexer;
        private bool disposed;
        private Dictionary<ObjectIdentifier, int> objectOffsets = new Dictionary<ObjectIdentifier, int>();
        private Dictionary<ObjectIdentifier, ObjectStreamReference> objectStreamRefs = new Dictionary<ObjectIdentifier, ObjectStreamReference>();
        private Dictionary<ObjectIdentifier, Function> functions = new Dictionary<ObjectIdentifier, Function>();
        private Dictionary<ObjectIdentifier, FontBase> fonts = new Dictionary<ObjectIdentifier, FontBase>();

        private DictionaryObject documentCatalog;
        private DictionaryObject pageTree;

        private PdfParser(Stream stream)
        {
            this.lexer = new PdfLexer(stream);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (!this.disposed) {
                this.disposed = true;
                this.lexer.Dispose();
            }
        }

        /// <summary>
        /// Parses the PDF document from specified stream.
        /// </summary>
        /// <param name="stream">The stream.</param>
        /// <returns></returns>
        public static PdfDocument ParseDocument(Stream stream)
        {
            var parser = new PdfParser(stream);
            return parser.ParseDocument();
        }

        private PdfDocument ParseDocument()
        {
            var document = new PdfDocument(this) {
                PdfVersion = ParsePdfVersion()
            };

            // read cross-references and trailer
            var offset = ParseLastXRefSectionOffset();
            var trailer = ParseXRefSection(offset);

            // check document encryption
            var encrypt = trailer.Get<DictionaryObject>("Encrypt", null);
            if (encrypt != null) {
                var filter = encrypt.Get<string>("Filter");
                var v = encrypt.Get<int>("V", 1);
                throw new PdfParseException(Resources.UnsupportedEncryption, filter, v);
            }

            this.documentCatalog = trailer.Get<DictionaryObject>("Root");
            this.pageTree = this.documentCatalog.Get<DictionaryObject>("Pages");

            var id = trailer.Get<ArrayObject>("ID", null);
            if (id != null) {
                document.FileIdentifier = id.ToArray<byte[]>();
            }
            document.PageCount = this.pageTree.Get<int>("Count");

            return document;
        }

        private DictionaryObject GetPageObject(DictionaryObject ancestor, int pageNumber, int start)
        {
            var count = ancestor.Get<int>("Count");
            var kids = ancestor.Get<ArrayObject>("Kids");
            if (count == kids.Count) {
                var page = kids.Get<DictionaryObject>(pageNumber - start);

                // copy all inheritable properties of the ancestor to this page
                CopyInheritablePageAttributes(ancestor, page);

                return page;
            }

            for (int i = 0; i < kids.Count; i++) {
                var node = kids.Get<DictionaryObject>(i);
                if (node.Type == "Pages") {
                    int nodePageCount = node.Get<int>("Count");
                    if (pageNumber >= start && pageNumber < start + nodePageCount) {
                        // copy all inheritable properties of the ancestor to this page node
                        CopyInheritablePageAttributes(ancestor, node);

                        return GetPageObject(node, pageNumber, start);
                    }
                    start += nodePageCount;
                }
            }

            throw new PdfParseException(Resources.PageNotFound, pageNumber);
        }

        private static Dictionary<string, object> CreateInheritablePageAttributes(params string[] names)
        {
            var attributes = new Dictionary<string, object>();
            foreach (var name in names) {
                attributes.Add(name, null);
            }
            return attributes;
        }


        private static void CopyInheritablePageAttributes(DictionaryObject source, DictionaryObject target)
        {
            foreach (var key in source.AllKeys) {
                if (inheritablePageAttributes.ContainsKey(key)) {
                    target[key] = source[key];
                }
            }
        }

        private Version ParsePdfVersion()
        {
            var stream = this.lexer.Stream;
            stream.Seek(0, SeekOrigin.Begin);
            stream.Match('%', 'P', 'D', 'F', '-');

            var v = stream.ReadBytes(3);
            int vMajor = v[0] - '0';
            int vMinor = v[2] - '0';

            if (vMajor == 1 && vMinor >= 0 || vMinor <= 7) {
                return new Version(vMajor, vMinor);
            }

            throw new PdfParseException(Resources.UnsupportedPdfVersion, vMajor, vMinor);
        }

        private long ParseLastXRefSectionOffset()
        {
            var stream = this.lexer.Stream;

            stream.Seek(-32, SeekOrigin.End);
            byte[] data = stream.ReadBytes(32);
            long offset = 0;
            long start = 1;
            for (var i = data.Length - 1; i >= 0; i--) {
                if (data[i] == '\r' || data[i] == '\n') {
                    if (offset == 0) {
                        continue;
                    }
                    break;
                }
                if (data[i] >= '0' && data[i] <= '9') {
                    offset += start * (data[i] - '0');
                    start *= 10;
                }
            }
            return offset;
        }

        private DictionaryObject ParseXRefSection(long offset)
        {
            DictionaryObject trailer = null;

            this.lexer.Seek(offset, SeekOrigin.Begin);
            var token = this.lexer.Read();
            if (token.IsMatch(TokenType.Keyword, "xref")) {
                // cross-reference table
                this.lexer.ReadWhitespace();
                while (this.lexer.Peek().Type == TokenType.Number) {
                    ParseXRefSubsection();
                }

                var trailerToken = this.lexer.Peek();
                if (trailerToken.IsMatch(TokenType.Keyword, "trailer")) {
                    // read trailer dictionary
                    this.lexer.Read();
                    trailer = this.lexer.ReadDictionaryObject(this);
                }
            }
            else if (token.Type == TokenType.Number) {
                // cross-reference stream (>= PDF 1.5)
                this.lexer.Read();
                this.lexer.Read(TokenType.Keyword, "obj");

                trailer = this.lexer.ReadDictionaryObject(this);
                ParseXRefStreamData(trailer);
            }
            else {
                throw new PdfParseException(Resources.UnexpectedToken, token);
            }

            if (trailer == null) {
                throw new PdfParseException(Resources.MissingTrailerDictionary);
            }
            // read previous cross-reference section
            int prevOffset = trailer.Get<int>("Prev", -1);
            if (prevOffset != -1) {
                ParseXRefSection(prevOffset);
            }

            return trailer;
        }

        private void ParseXRefSubsection()
        {
            var start = this.lexer.Read().ParseInteger();
            var count = this.lexer.Read().ParseInteger();

            lexer.ReadWhitespace();

            for (int i = 0; i < count; i++) {
                byte[] data = this.lexer.ReadBytes(20);
                if (data[17] == 'n') {
                    var offset = ParserUtility.ParseAsciiInt32(data, 0, 10);
                    var generation = ParserUtility.ParseAsciiInt32(data, 11, 5);
                    var identifier = new ObjectIdentifier(start + i, generation);
                    this.objectOffsets[identifier] = offset;
                }
            }
        }

        private void ParseXRefStreamData(DictionaryObject dictionary)
        {
            var w = dictionary.Get<ArrayObject>("W").ToArray<int>();
            var index = new int[] { 0, dictionary.Get<int>("Size") };
            ArrayObject indexArray;
            if (dictionary.TryGet<ArrayObject>("Index", out indexArray)) {
                index = indexArray.ToArray<int>();
            }

            // calculate length of each entry
            var length = 0;
            for (int i = 0; i < w.Length; i++) {
                length += w[i];
            }

            using (var stream = ParseStreamObject(dictionary).OpenFilteredStream()) {
                for (int i = 0; i < index.Length; i += 2) {
                    var start = index[i];
                    var count = index[i + 1];

                    for (int j = 0; j < count; j++) {
                        var entry = new byte[length];
                        if (stream.Read(entry, 0, length) != length) {
                            throw new PdfParseException(Resources.UnexpectedEndOfCrossRefStream);
                        }

                        var type = 1;
                        if (w[0] > 0) {
                            type = ParserUtility.ParseInt32(entry, 0, w[0]);
                        }

                        if (type == 1) {
                            var offset = ParserUtility.ParseInt32(entry, w[0], w[1]);
                            var generation = 0;
                            if (w[2] > 0) {
                                generation = ParserUtility.ParseInt32(entry, w[0] + w[1], w[2]);
                            }
                            var identifier = new ObjectIdentifier(start + j, generation);

                            this.objectOffsets[identifier] = offset;
                        }
                        else if (type == 2) {
                            var objStreamNr = ParserUtility.ParseInt32(entry, w[0], w[1]);
                            var objStreamIdentifier = new ObjectIdentifier(objStreamNr, 0);
                            var objIndex = ParserUtility.ParseInt32(entry, w[0] + w[1], w[2]);

                            var identifier = new ObjectIdentifier(start + j, 0);    // object in object streams always have generation 0

                            this.objectStreamRefs[identifier] = new ObjectStreamReference(objStreamIdentifier, objIndex);
                        }
                        else {
                            // other type is interpreted as a reference to the null object.
                        }
                    }
                }
            }
        }

        private object ParseObjectAtOffset(ObjectIdentifier identifier, int offset)
        {
            // read object at offset
            this.lexer.Seek(offset, SeekOrigin.Begin);

            // match indirect object header
            this.lexer.Read(TokenType.Number, identifier.Number.ToString());
            this.lexer.Read(TokenType.Number, identifier.Generation.ToString());
            this.lexer.Read(TokenType.Keyword, "obj");

            return this.lexer.ReadObject(this);
        }

        private object ParseObjectFromObjectStream(ObjectIdentifier identifier, ObjectStreamReference reference)
        {
            var dictionary = ParseIndirectObject<DictionaryObject>(reference.StreamIdentifier);
            var n = dictionary.Get<int>("N");
            var first = dictionary.Get<int>("First");

            using (var stream = ParseStreamObject(dictionary).OpenFilteredStream()) {
                var lexer = new PdfLexer(stream);

                // skip to index
                for (int i = 0; i < reference.Index; i++) {
                    lexer.Read(TokenType.Number);
                    lexer.Read(TokenType.Number);
                }
                var objectNr = lexer.Read(TokenType.Number).ParseInteger();
                var offset = lexer.Read(TokenType.Number).ParseInteger();

                if (objectNr != identifier.Number) {
                    throw new PdfParseException(Resources.ObjectNotFound, identifier);
                }
                lexer.Seek(first + offset, SeekOrigin.Begin);

                return lexer.ReadObject(this);
            }
        }

        internal string ParsePage(int pageNumber, LoadSettings settings)
        {
            DateTime start = DateTime.Now;
            var pageObject = GetPageObject(this.pageTree, pageNumber, 1);
            var pageParser = new PageParser();

            var xaml = pageParser.Parse(pageObject, settings);
            Debug.WriteLine("Parsing page {0} took {1}ms", pageNumber, (DateTime.Now - start).TotalMilliseconds);

            return xaml;
        }

        internal T ParseIndirectObject<T>(ObjectIdentifier identifier)
        {
            object o;
            int offset;
            if (this.objectOffsets.TryGetValue(identifier, out offset)) {
                // read object at offset
                o = ParseObjectAtOffset(identifier, offset);
            }
            else {
                // read object from object stream
                ObjectStreamReference objRef;
                if (!this.objectStreamRefs.TryGetValue(identifier, out objRef)) {
                    throw new PdfParseException(Resources.ObjectNotFound, identifier);
                }
                o = ParseObjectFromObjectStream(identifier, objRef);
            }

            // store identifier with object
            var indirectObj = o as PdfObject;
            if (indirectObj != null) {
                indirectObj.Identifier = identifier;
            }

            return ParserUtility.Cast<T>(o);
        }

        internal StreamObject ParseStreamObject(DictionaryObject dictionary)
        {
            // ensure the stream is at the correct position
            this.lexer.Seek(dictionary.StreamEndPosition, SeekOrigin.Begin);
            this.lexer.Read(TokenType.Keyword, "stream");
            this.lexer.ReadStreamObjectEol();

            return new StreamObject(dictionary, this.lexer.Stream);
        }

        internal Function GetFunction(DictionaryObject dictionary)
        {
            // if dictionary is not an indirect object, no identifier exists and the function will not be cached

            Function function;
            if (!dictionary.IsIndirectObject || !this.functions.TryGetValue(dictionary.Identifier.Value, out function)) {
                // create function
                function = Function.CreateFunction(this, dictionary);

                if (dictionary.IsIndirectObject) {
                    // cache function
                    this.functions[dictionary.Identifier.Value] = function;
                }
            }

            return function;
        }

        internal FontBase GetFont(DictionaryObject dictionary)
        {
            if (dictionary.Identifier == null) {
                throw new PdfParseException(Resources.FontIdentifierNotFound);
            }
            var identifier = dictionary.Identifier.Value;

            FontBase font;
            if (!this.fonts.TryGetValue(identifier, out font)) {
                // create font
                font = FontBase.CreateFont(dictionary);

                // cache font (can be null, if not supported)
                this.fonts[identifier] = font;
            }
            return font;
        }

        internal FontBase GetFont(ObjectIdentifier identifier)
        {
            FontBase font;
            if (this.fonts.TryGetValue(identifier, out font)) {
                return font;
            }
            return null;
        }
    }
}
